/**
 *  利用react hook 编写拖拽组件，依赖于父元素的position必须是relative。
 *  @props onStartDrag 拖拽开始时会执行的函数
 *  @props onDragEnd 拖拽结束会执行的函数，会把最终的位置抛出去
 *  @props top 元素的top距离
 *  @props left 元素的left距离
 */

import {useState, useCallback, useMemo, useEffect, useRef} from 'react';
import PropsType from "prop-types";

const POSITION = {};

const Draggable = ({children, onStartDrag, onDragEnd, top, left, domItem}) => {
  const rootRef = useRef()
  const offsetLeft = useRef(0)
  const offsetTop = useRef(0)

  const getOffsetLeft = useCallback((obj) => { // 找到某个元素距离最左边的距离
    let tmp = obj.offsetLeft;
    let val = obj.offsetParent;
    while (val != null) {
      tmp += val.offsetLeft;
      val = val.offsetParent;
    }
    return tmp;
  }, [])

  const getOffsetTop = useCallback((obj) => { // 找到某个元素距离最上方的距离
    let tmp = obj.offsetTop;
    let val = obj.offsetParent;
    while (val != null) {
      tmp += val.offsetTop;
      val = val.offsetParent;
    }
    return tmp;
  }, [])
  
  const [state, setState] = useState({
    isDragging: false,
    translation: top && left ? { x: left, y: top } : POSITION
  });
  
  const handleMouseDown = useCallback(() => {
    setState(state => ({
      ...state,
      isDragging: true
    }));
    offsetLeft.current = getOffsetLeft(rootRef.current.parentNode) // 获取父节点距离页面最左侧的距离
    offsetTop.current = getOffsetTop(rootRef.current.parentNode) // 获取父节点距离最上方的距离
  }, []);
    
  const handleMouseMove = useCallback((event) => {
    const width = children.ref.current.clientWidth;
    const height = children.ref.current.clientHeight;
    // 控制元素当前的位置--计算方式：当前鼠标的位置-父节点距离最左侧的距离 = 鼠标当前在容器里的位置。 再减去宽度高度的一半让鼠标始终在元素的中间位置
    const translation = { x: event.clientX - offsetLeft.current - (width / 2), y: event.clientY - offsetTop.current - (height / 2) };
    // 边界控制
    if (translation.x < 0) {
      translation.x = 0;
    }
    if (translation.y < 0) {
      translation.y = 0;
    }
    if (translation.x + width > rootRef.current.parentNode.clientWidth) {
      // window.innerWidth
      translation.x = rootRef.current.parentNode.clientWidth - width;
    }
    if (translation.y + height > rootRef.current.parentNode.clientHeight) {
      translation.y = rootRef.current.parentNode.clientHeight - height;
    }
    
    setState(state => ({
      ...state,
      translation
    }));
  }, []);
    
  const handleMouseUp = useCallback(() => {
    setState(state => ({
      ...state,
      isDragging: false
    }));
  }, []);

  useEffect(() => {
    if (state.isDragging) {
      onStartDrag(domItem, state.translation)
      window.addEventListener('mousemove', handleMouseMove);
      window.addEventListener('mouseup', handleMouseUp);
    } else {
      window.removeEventListener('mousemove', handleMouseMove);
      window.removeEventListener('mouseup', handleMouseUp);
      onDragEnd(domItem, state.translation);
    }
  }, [state.isDragging, handleMouseMove, handleMouseUp]);

  const styles = useMemo(() => ({
    cursor: state.isDragging ? '-webkit-grabbing' : '-webkit-grab',
    left: state.translation.x,
    top: state.translation.y,
    transition: state.isDragging ? 'none' : 'transform 100ms',
    zIndex: state.isDragging ? 2 : 1,
    position: 'absolute'
  }), [state.isDragging, state.translation]);
    
  return (
    <div style={styles} ref={rootRef} onMouseDown={handleMouseDown}>
      {children}
    </div>
  );
};

Draggable.propTypes = {
  onStartDrag: PropsType.func,
  onDragEnd: PropsType.func,
  left:  PropsType.oneOfType([PropsType.number, PropsType.string]),
  top:  PropsType.oneOfType([PropsType.number, PropsType.string]),
  domItem: PropsType.oneOfType([PropsType.number, PropsType.string, PropsType.object])
}
 
export default Draggable;